New in PyTeal: Boxes and ABI-Compatible Smart Contracts¶

image.png

Learn how to utilize unlimited global storage and ABI-compatibility in PyTeal, Algorand’s library for writing smart contracts in Python.¶

Agenda
¶

I. Algorand's PyTeal¶

II. Boxes¶

III. The ABI-Router¶

IV. Demo¶

QR Code to github including slides & all is here:¶

image.png

.

Links
¶

  1. Decipher 2021: Writing Smart Contracts with Python
    • Slides
    • Video
  2. PyTeal's ReadTheDocs
    • Box Storage
    • Bare App Calls
    • ABI Types
  3. Algorand Developer Portal
    • Application Call Transactions
    • OnComplete Actions
    • Smart Contract Storage
  4. ARC-4
  5. Box / AVM-8 consensus params
  6. Beaker
    • Hello Beaker
    • github Repo

image.png

.

Algorand dApp Layers
¶

image.png

PyTeal: Boxes and ABI-Compatible Smart Contracts¶

image.png

Polling App
¶

image.png

Favorite Tall Building?¶

1. Empire State - New York¶

2. Burj Khalifa - Dubai¶

3. Abraj Al-Bait - Mecca¶

4. Taipei 101 - Taipei¶

5. Shanghai Tower - Shanghai¶

6. Merdeka 118 - Kuala Lumpur¶

7. Other¶

image.png

.

Let's Take a Poll On Chain¶

image.png

.

Poll administrator can:¶

create and delete the poll application¶

open and close a poll for voting¶

Anyone can:¶

submit a choice¶

get the poll status¶

image.png

.

Example¶

submit choice Burj Khalifa - Dubai¶

After 100 submissions¶

status =¶

  • 11 chose Empire State - New York
  • 35 chose Burj Khalifa - Dubai
  • 22 chose Abraj Al-Bait - Mecca
  • ...

image.png

.

Problem: We don't want any account to have more influence than other accounts¶

OK: Toma¶

submit choice Taipei 101 - Taipei¶

END-RESULT: ✅ Taipei += 1 ✅¶

image.png

.

OK: Sally¶

a. submit choice Taipei 101 - Taipei¶

b. change mind and re-submit choice Shanghai Tower - Shanghai¶

END-RESULT: ✅ Shanghai += 1 ✅¶

image.png

.

NOT OK: Maryam¶

a. submit choice for Merdeka 118 - Kuala Lumpur¶

b. submit ADDITIONAL choice for Merdeka 118¶

. . .¶

z. submit ADDITIONAL choice for Merdeka 118¶

END-RESULT: ❌ Merdeka += 26 ❌¶

image.png

.

In [4]:
%%script false --no-raise-error 

# Dead End

%pip install networkx
%pip install matplotlib

import networkx as nx
import matplotlib.pyplot as poll
import random
from networkx.algorithms import bipartite
CHOICES = 7
LAST = 10

choices = list(range(CHOICES))
accounts = [f"{' '*10}ACC {chr(x)}" for x in range(ord('A'), ord('A') + LAST)]

B = nx.Graph()
B.add_nodes_from(choices, bipartite=0)
B.add_nodes_from(accounts, bipartite=1)

# print(choices, accounts)
B.add_edges_from([(random.randint(0, CHOICES-1), acc) for acc in accounts])
pos = dict()
pos.update( (n, (1, LAST - i)) for i, n in enumerate(choices) ) 
pos.update( (n, (2, LAST - i)) for i, n in enumerate(accounts) ) 

# print(pos)
nx.draw_networkx(B, pos=pos, node_size=200)
nx.draw_networkx_nodes(B, pos=pos, nodelist=choices, node_size=300)
poll.axis('off')
poll.show()

Each account only contributes to $\leq$ 1 choice¶

image.png

image.png

.

NOTES:

  • Answers that may come up:
    • Can't keep all this info in global storage
      • Stats - 8KB total possible. Assuming no compression and 32B per address, that's at most 256 accounts we can keep track of
    • Can keep info in local - but then there are some downsides:
      • voters need to opt into poll (MBR issues)
      • need to have policies and code around closing/clearing out
    • TRICKY Sybil attack. Why can't some create tons of accounts and vote many times. AND it's even easier to pull that off now that you don't need to

Each account only contributes to $\leq$ 1 choice¶

HOW ?
¶

Can we store each account in global storage?¶

https://github.com/algorand/go-algorand/blob/995ae47e80c50e7632034cac8a70b7d6434d03e3/config/consensus.go#L969-L970

64 keys X (64 + 64 bytes) == 8,196 bytes

Yes, BUT limited to 8KB which is only 256 accounts (if not compressing)¶

image.png

.

HOW ?
¶

Can we keep track of an account's vote in local storage?¶

Yes, BUT requires opt in¶

clear state forcibly erases submission record¶

min MBR = 0.15 Algos with 1 local¶

complex code/policies for opt in, close out, clear state¶

image.png

.

Anything else we could do (now on mainnet) ?¶

UNLIMITED assets and apps can be associated with any account, including poll's app account¶

image.png

Could hack these to store votes¶

complicated, error prone, expensive¶

Notes:

  • expensive 0.1 Algos / acct vs. 0.0157 Algos / acct w/ boxes
  • complicated / error prone
    • need to keep track of asset id off chain and supply it as a foreign ref, then parse out URL, verify that first 32 bytes are the account, etc...
    • need to group with Asset Config Txn which specifies how the URL changes

image.png

.

SOLUTION
¶

image.png

Notes:

  • In this solution, only need
    • 32 bytes for the Key (max is 64 bytes)
    • 1 byte for the value (max is 32 KB)
  • Costs in terms of MBR formula of 2500 + 400*(len(key) + len(value))
    • 15.7 mili-Algo's to store submission info per account
    • Each txn allows up to 8 box references in it with the size of boxes touched totalling at most 1KB * # of refs
    • In our case, this is not an issue. A single box reference -being the submission account- will suffice

image.png

.

Boxes in PyTeal¶

TODO!!!!¶

In [5]:
%%script false --no-raise-error

choice = Bytes(b"\x01") # "2. Burj Khalifa - Dubai
option_count_prefix = b"option_count_" # TODO: replace the option_count_keys[0] stuff below


new_choice_count_key = ScratchVar(TealType.bytes)
old_choice_count_key = ScratchVar(TealType.bytes)
submission_expr = Seq(
        Assert(Btoi(choice) < Int(7)),
        new_choice_count_key.store(
            SetByte(option_count_keys[0], Int(len(option_count_prefix)), choice.get())
        ),
        # ### BOXES BEGIN ### #
        sender_box := App.box_get(Txn.sender()),
        If(sender_box.hasValue()).Then(
            # the sender has already submitted a response, so it must be cleared
            Assert(App.globalGet(resubmit_key)),
            old_choice_count_key.store(
                SetByte(
                    option_count_keys[0],
                    Int(len(option_count_prefix)),
                    Btoi(sender_box.value()),
                )
            ),
            App.globalPut(
                old_choice_count_key.load(),
                App.globalGet(old_choice_count_key.load()) - Int(1),
            ),
        ),
        App.box_put(Txn.sender(), choice.encode()),
        # ### BOXES END ### #
        App.globalPut(
            new_choice_count_key.load(),
            App.globalGet(new_choice_count_key.load()) + Int(1),
        ),
    )

methods - allow interacting with Poll App¶

* create - called only during app creation and only by administrator¶

* open - called only by administrator¶

* close - called only by administrator¶

* submit¶

* status¶

(didn't include delete for reasons to be explained shortly)¶

image.png

.

Refresher - OnComplete Actions for App Transactions¶

Value Name Description
0 NoOp Execute ApprovalProgram only
1 OptIn Allocate local state and execute ApprovalProgram
2 CloseOut Execute ApprovalProgram and clear local state
3 ClearState Execute ClearStateProgram and clear locals (even if rejects)
4 UpdateApplication Execute ApprovalProgram and update programs
5 DeleteApplication Execute ApprovalProgram and delete the app

image.png

.

PyTeal's Router: Building an ARC-4 Application¶

Definition: method¶

An ABI method is a section of code called externally through an application transaction whose first argument is the method's selector¶

image.png

.

Definition: Bare app call action¶

An ABI bare app call is an application transaction with zero arguments and its associated action is the section of code to be executed¶

image.png

.

Definition: a Router and its M.O.E. Questions¶

image.png

image.png

.

Definition: a Router and its M.O.E. Questions¶

PyTeal's Router constructs the Teal code necessary to delegate application transactions to either a bare app call action or a method based on answers to:¶

1 [M]. In a bare app call? If not, which method selected?¶

2 [O]. Which OnComplete is requested?¶

3 [E]. This app already exists? (Conversely, being created?)¶

image.png

.

PyTeal's Router: Building an ARC-4 Application¶

image.png

.

image.png

image.png

.

image.png

image.png

.

image.png

image.png

.

Router Initialization¶

image.png

WARNING

In [6]:
# WARNING: STUBS ARE FOR ROUTER-ILLUSTRATION PURPOSES ONLY!!!

del_action = OnCompleteAction.call_only(Seq()) 

router = Router(
    name="OpenPollingApp",
    descr="This is a polling application.",
    bare_calls=BareCallActions(delete_application=del_action),
)

image.png

.

In [7]:
approval, clear, json_contract = router.compile_program(version=8)

Compile¶

approval, clear, json_contract = router.compile_program(version=8)

JSON Contract (with no methods)¶

In [8]:
print(json.dumps(json_contract.dictify(), indent=2))
{
  "name": "OpenPollingApp",
  "methods": [],
  "networks": {},
  "desc": "This is a polling application."
}

image.png

.

Compile¶

approval, clear, json_contract = router.compile_program(version=8)
In [9]:
print(approval)
#pragma version 8
txn NumAppArgs
int 0
==
bnz main_l2
err
main_l2:
txn OnCompletion
int DeleteApplication
==
bnz main_l4
err
main_l4:
txn ApplicationID
int 0
!=
assert
int 1
return

image.png

.

In [10]:
%%script false --no-raise-error #pragma version 8
txn NumAppArgs
int 0
==
bnz main_l2
err
main_l2:
txn OnCompletion
int DeleteApplication
==
bnz main_l4
err
main_l4:
txn ApplicationID
int 0
!=
assert
int 1
return

Routing bare app call delete¶

In [11]:
show.prepare(approval, (2, 5), (7, 11), (13, 19))
In [12]:
show() # custom method showing relevant Teal
txn NumAppArgs
int 0
==
bnz main_l2
. . .
main_l2:
txn OnCompletion
int DeleteApplication
==
bnz main_l4
. . .
main_l4:
txn ApplicationID
int 0
!=
assert
int 1
return

image.png

.

Append methods to route with @router.method¶

image.png

image.png

.

Route open() and close() methods¶

* no args or output¶

* OnComplete == NoOp and not during create¶

Note:

PyTeal lets us name each method with a the Python reserved word (open and close) via the name param

In [13]:
@router.method(name="open")
def open_poll() -> Expr:
    """Marks this poll as open."""
    return Seq()

@router.method(name="close")
def close_poll() -> Expr:
    """Marks this poll as closed."""
    return Seq()

WARNING

image.png

.

Compile¶

In [14]:
approval, clear, json_contract = router.compile_program(version=8)

JSON Contract Stub (with 2 simple method)¶

Now the methods do get filled with information.

In [15]:
print(json.dumps(json_contract.dictify(), indent=2))
{
  "name": "OpenPollingApp",
  "methods": [
    {
      "name": "open",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as open."
    },
    {
      "name": "close",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as closed."
    }
  ],
  "networks": {},
  "desc": "This is a polling application."
}
In [16]:
# print(approval)
In [17]:
%%script false --no-raise-error #pragma version 8
txn NumAppArgs
int 0
==
bnz main_l6
txna ApplicationArgs 0
method "open()void"
==
bnz main_l5
txna ApplicationArgs 0
method "close()void"
==
bnz main_l4
err
main_l4:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub close_1
int 1
return
main_l5:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub open_0
int 1
return
main_l6:
txn OnCompletion
int DeleteApplication
==
bnz main_l8
err
main_l8:
txn ApplicationID
int 0
!=
assert
int 1
return

// open
open_0:
retsub

// close
close_1:
retsub

Approval Program - Routing to open and close¶

In [18]:
show.prepare(approval, (6, 9), (10, 13), (27, 36), (15, 24), (53, 55), (57, 59))

Notes:

  • method pseudo-opcode computes the method selector (SHA512-256 of the bytes)
  • router ask Q1 by comparing with txna ApplicationArgs 0 (method args start at txn ApplicationArgs 1) and branches to relevant label
  • then asks Q2 & Q3 by asserting thet OnCompletion == NoOp and already existing ApplicatoinId != 0
  • then routes to actual subroutine that implements the method with a callsub
In [19]:
show()
txna ApplicationArgs 0
method "open()void"
==
bnz main_l5
. . .
txna ApplicationArgs 0
method "close()void"
==
bnz main_l4
. . .
main_l5:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub open_0
. . .
main_l4:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub close_1
. . .
// open
open_0:
retsub
. . .
// close
close_1:
retsub

ABI Types¶

image.png

.

A Selection of PyTeal's Basic ABI Types¶

PyTeal Type

ARC-4 Type

Dynamic / Static

Description

abi.Uint8

uint8

Static

An 8-bit unsigned integer

abi.StaticArray[T,N]

T[N]

IFF T is

A fixed-length array with N elements

abi.String

string

Dynamic

Variable-length byte array

There are more!¶

Uint16/32/64, Bool, Byte, DynamicArray, Tuple, ...¶

Link: PyTeal ABI Types
¶

image.png

.

Add a method with ABI arguments but no returns¶

Notes

  • utilizes choice: abi.Uint8
  • take a mental note of the Docstring """Submit ... """
    • The very first line becomes the desc field of the method in the JSON contract
In [20]:
@router.method
def submit(choice: abi.Uint8) -> Expr:
    """Submit a response to the poll.
    Args:
        choice: The choice made by the sender.
    """
    return Seq()

image.png

.

Compile¶

In [21]:
opts = OptimizeOptions(scratch_slots=True)
approval, clear, json_contract = router.compile_program(version=8, optimize=opts)
# -----------------------------------------------------------------^^^^^^^^^^^^^
In [22]:
print(json.dumps(json_contract.dictify(), indent=2))
{
  "name": "OpenPollingApp",
  "methods": [
    {
      "name": "open",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as open."
    },
    {
      "name": "close",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as closed."
    },
    {
      "name": "submit",
      "args": [
        {
          "type": "uint8",
          "name": "choice",
          "desc": "The choice made by the sender."
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Submit a response to the poll."
    }
  ],
  "networks": {},
  "desc": "This is a polling application."
}
In [23]:
%%script false --no-raise-error #{
  "name": "OpenPollingApp",
  "methods": [
    {
      "name": "open",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as open."
    },
    {
      "name": "close",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as closed."
    },
    {
      "name": "submit",
      "args": [
        {
          "type": "uint8",
          "name": "choice",
          "desc": "The choice made by the sender."
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Submit a response to the poll."
    }
  ],
  "networks": {},
  "desc": "This is a polling application."
}

JSON Contract - method submit()¶

Notes:

  • desc is coming from the defining Python function's Docstring
In [25]:
show() # lines 20-33 of the JSON contract
    {
      "name": "submit",
      "args": [
        {
          "type": "uint8",
          "name": "choice",
          "desc": "The choice made by the sender."
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Submit a response to the poll."
    }

image.png

.

In [27]:
%%script false --no-raise-error #pragma version 8
txn NumAppArgs
int 0
==
bnz main_l8
txna ApplicationArgs 0
method "open()void"
==
bnz main_l7
txna ApplicationArgs 0
method "close()void"
==
bnz main_l6
txna ApplicationArgs 0
method "submit(uint8)void"
==
bnz main_l5
err
main_l5:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
txna ApplicationArgs 1
int 0
getbyte
callsub submit_2
int 1
return
main_l6:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub close_1
int 1
return
main_l7:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub open_0
int 1
return
main_l8:
txn OnCompletion
int DeleteApplication
==
bnz main_l10
err
main_l10:
txn ApplicationID
int 0
!=
assert
int 1
return

// open
open_0:
retsub

// close
close_1:
retsub

// submit
submit_2:
store 0
retsub
In [28]:
show.prepare(approval, (14, 17), (19, 31), (80, 83))

Routing submit(choice)¶

In [29]:
show()
txna ApplicationArgs 0
method "submit(uint8)void"
==
bnz main_l5
. . .
main_l5:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
txna ApplicationArgs 1
int 0
getbyte
callsub submit_2
. . .
// submit
submit_2:
store 0
retsub

OMIT THE REST OF THE STUBS FROM PRESENTATION

Notes:

  • @router.method decorator is signalled about looking for an OnComplete==NoOp during an app create
  • introducing types abi.StaticArray[abi.String, Literal[3]], abi.Bool
  • the first is recursively "dynamic type"
    • dynamic means that the size of data being encoded is not fixed
  • ARC-4 specifies exactly how each type is encoded

image.png

.

Compile¶

Approval Program Stub (with 2 simple methods + 1 non-returning OnComplete=NoOp + 1 non-returning during creation)¶

In [32]:
# print(approval)
In [33]:
%%script false --no-raise-error #pragma version 8
txn NumAppArgs
int 0
==
bnz main_l10
txna ApplicationArgs 0
method "open()void"
==
bnz main_l9
txna ApplicationArgs 0
method "close()void"
==
bnz main_l8
txna ApplicationArgs 0
method "submit(uint8)void"
==
bnz main_l7
txna ApplicationArgs 0
method "create(string[3],bool)void"
==
bnz main_l6
err
main_l6:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
==
&&
assert
txna ApplicationArgs 1
store 1
txna ApplicationArgs 2
int 0
int 8
*
getbit
store 2
load 1
load 2
callsub create_3
int 1
return
main_l7:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
txna ApplicationArgs 1
int 0
getbyte
callsub submit_2
int 1
return
main_l8:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub close_1
int 1
return
main_l9:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub open_0
int 1
return
main_l10:
txn OnCompletion
int DeleteApplication
==
bnz main_l12
err
main_l12:
txn ApplicationID
int 0
!=
assert
int 1
return

// open
open_0:
retsub

// close
close_1:
retsub

// submit
submit_2:
store 0
retsub

// create
create_3:
store 4
store 3
retsub

image.png

.

The Actual Program¶

NOTES:

  • make sure to put the final version here
In [36]:
def on_delete() -> Expr:
    return Assert(Txn.sender() == Global.creator_address())


router = Router(
    name="OpenPollingApp",
    descr="A polling application with no restrictions on who can participate.",
    bare_calls=BareCallActions(
        delete_application=OnCompleteAction.call_only(on_delete())
    ),
)

open_key = Bytes(b"open")
resubmit_key = Bytes(b"resubmit")
option_name_prefix = b"option_name_"
option_name_keys = [
    Bytes(option_name_prefix + b"\x00"),
    Bytes(option_name_prefix + b"\x01"),
    Bytes(option_name_prefix + b"\x02"),
]
option_count_prefix = b"option_count_"
option_count_keys = [
    Bytes(option_count_prefix + b"\x00"),
    Bytes(option_count_prefix + b"\x01"),
    Bytes(option_count_prefix + b"\x02"),
]


@router.method(no_op=CallConfig.CREATE)
def create(
    options: abi.StaticArray[abi.String, Literal[3]], can_resubmit: abi.Bool
) -> Expr:
    """Create a new polling application.

    Args:
        options: A list of options for the poll. This list should not contain duplicate entries.
        can_resubmit: Whether this poll allows accounts to change their submissions or not.
    """
    name = abi.make(abi.String)
    return Seq(
        App.globalPut(open_key, Int(0)),
        App.globalPut(resubmit_key, can_resubmit.get()),
        name.set(options[0]),
        App.globalPut(option_name_keys[0], name.get()),
        App.globalPut(option_count_keys[0], Int(0)),
        name.set(options[1]),
        App.globalPut(option_name_keys[1], name.get()),
        App.globalPut(option_count_keys[1], Int(0)),
        name.set(options[2]),
        App.globalPut(option_name_keys[2], name.get()),
        App.globalPut(option_count_keys[2], Int(0)),
    )


@router.method(name="open")
def open_poll() -> Expr:
    """Marks this poll as open.

    This will fail if the poll is already open.

    The poll must be open in order to receive user input.
    """
    return Seq(
        Assert(Not(App.globalGet(open_key))),
        App.globalPut(open_key, Int(1)),
    )


@router.method(name="close")
def close_poll() -> Expr:
    """Marks this poll as closed.

    This will fail if the poll is already closed.
    """
    return Seq(
        Assert(App.globalGet(open_key)),
        App.globalPut(open_key, Int(0)),
    )


@router.method
def submit(choice: abi.Uint8) -> Expr:
    """Submit a response to the poll.

    Submissions can only be received if the poll is open. If the poll is closed, this will fail.

    If a submission has already been made by the sender and the poll allows resubmissions, the
    sender's choice will be updated to the most recent submission. If the poll does not allow
    resubmissions, this action will fail.

    Args:
        choice: The choice made by the sender. This must be an index into the options for this poll.
    """
    new_choice_count_key = ScratchVar(TealType.bytes)
    old_choice_count_key = ScratchVar(TealType.bytes)
    return Seq(
        Assert(choice.get() < Int(3)),
        new_choice_count_key.store(
            SetByte(option_count_keys[0], Int(len(option_count_prefix)), choice.get())
        ),
        # ### BOXES BEGIN ### #
        sender_box := App.box_get(Txn.sender()),
        If(sender_box.hasValue()).Then(
            # the sender has already submitted a response, so it must be cleared
            Assert(App.globalGet(resubmit_key)),
            old_choice_count_key.store(
                SetByte(
                    option_count_keys[0],
                    Int(len(option_count_prefix)),
                    Btoi(sender_box.value()),
                )
            ),
            App.globalPut(
                old_choice_count_key.load(),
                App.globalGet(old_choice_count_key.load()) - Int(1),
            ),
        ),
        App.box_put(Txn.sender(), choice.encode()),
        # ### BOXES END ### #
        App.globalPut(
            new_choice_count_key.load(),
            App.globalGet(new_choice_count_key.load()) + Int(1),
        ),
    )


class PollStatus(abi.NamedTuple):
    can_resubmit: abi.Field[abi.Bool]
    is_open: abi.Field[abi.Bool]
    results: abi.Field[abi.StaticArray[abi.Tuple2[abi.String, abi.Uint64], Literal[3]]]


@router.method
def status(*, output: PollStatus) -> Expr:
    """Get the status of this poll.

    Returns:
        A tuple containing the following information, in order: whether the poll allows
        resubmission, whether the poll is open, and an array of the poll's current results. This
        array contains one entry per option, and each entry is a tuple of that option's value and
        the number of accounts who have voted for it.
    """
    can_resubmit = abi.make(abi.Bool)
    is_open = abi.make(abi.Bool)
    option_name = abi.make(abi.String)
    option_count = abi.make(abi.Uint64)
    partial_results = [
        abi.make(abi.Tuple2[abi.String, abi.Uint64]),
        abi.make(abi.Tuple2[abi.String, abi.Uint64]),
        abi.make(abi.Tuple2[abi.String, abi.Uint64]),
    ]
    results = abi.make(abi.StaticArray[abi.Tuple2[abi.String, abi.Uint64], Literal[3]])
    return Seq(
        can_resubmit.set(App.globalGet(resubmit_key)),
        is_open.set(App.globalGet(open_key)),
        option_name.set(App.globalGet(option_name_keys[0])),
        option_count.set(App.globalGet(option_count_keys[0])),
        partial_results[0].set(option_name, option_count),
        option_name.set(App.globalGet(option_name_keys[1])),
        option_count.set(App.globalGet(option_count_keys[1])),
        partial_results[1].set(option_name, option_count),
        option_name.set(App.globalGet(option_name_keys[2])),
        option_count.set(App.globalGet(option_count_keys[2])),
        partial_results[2].set(option_name, option_count),
        results.set([partial_results[0], partial_results[1], partial_results[2]]),
        output.set(can_resubmit, is_open, results),
    )
In [39]:
show()
def on_delete() -> Expr:
    return Assert(Txn.sender() == Global.creator_address())


router = Router(
    name="OpenPollingApp",
    descr="A polling application with no restrictions on who can participate.",
    bare_calls=BareCallActions(
        delete_application=OnCompleteAction.call_only(on_delete())
    ),
)

open_key = Bytes(b"open")
resubmit_key = Bytes(b"resubmit")
option_name_prefix = b"option_name_"
option_name_keys = [
    Bytes(option_name_prefix + b""),
    Bytes(option_name_prefix + b""),
    Bytes(option_name_prefix + b""),
]
option_count_prefix = b"option_count_"
option_count_keys = [
    Bytes(option_count_prefix + b""),
    Bytes(option_count_prefix + b""),
    Bytes(option_count_prefix + b""),
]


@router.method(no_op=CallConfig.CREATE)
def create(
    options: abi.StaticArray[abi.String, Literal[3]], can_resubmit: abi.Bool
) -> Expr:
    """Create a new polling application.

    Args:
        options: A list of options for the poll. This list should not contain duplicate entries.
        can_resubmit: Whether this poll allows accounts to change their submissions or not.
    """
    name = abi.make(abi.String)
    return Seq(
        App.globalPut(open_key, Int(0)),
        App.globalPut(resubmit_key, can_resubmit.get()),
        name.set(options[0]),
        App.globalPut(option_name_keys[0], name.get()),
        App.globalPut(option_count_keys[0], Int(0)),
        name.set(options[1]),
        App.globalPut(option_name_keys[1], name.get()),
        App.globalPut(option_count_keys[1], Int(0)),
        name.set(options[2]),
        App.globalPut(option_name_keys[2], name.get()),
        App.globalPut(option_count_keys[2], Int(0)),
    )


@router.method(name="open")
def open_poll() -> Expr:
    """Marks this poll as open.

    This will fail if the poll is already open.

    The poll must be open in order to receive user input.
    """
    return Seq(
        Assert(Not(App.globalGet(open_key))),
        App.globalPut(open_key, Int(1)),
    )


@router.method(name="close")
def close_poll() -> Expr:
    """Marks this poll as closed.

    This will fail if the poll is already closed.
    """
    return Seq(
        Assert(App.globalGet(open_key)),
        App.globalPut(open_key, Int(0)),
    )


@router.method
def submit(choice: abi.Uint8) -> Expr:
    """Submit a response to the poll.

    Submissions can only be received if the poll is open. If the poll is closed, this will fail.

    If a submission has already been made by the sender and the poll allows resubmissions, the
    sender's choice will be updated to the most recent submission. If the poll does not allow
    resubmissions, this action will fail.

    Args:
        choice: The choice made by the sender. This must be an index into the options for this poll.
    """
    new_choice_count_key = ScratchVar(TealType.bytes)
    old_choice_count_key = ScratchVar(TealType.bytes)
    return Seq(
        Assert(choice.get() < Int(3)),
        new_choice_count_key.store(
            SetByte(option_count_keys[0], Int(len(option_count_prefix)), choice.get())
        ),
        # ### BOXES BEGIN ### #
        sender_box := App.box_get(Txn.sender()),
        If(sender_box.hasValue()).Then(
            # the sender has already submitted a response, so it must be cleared
            Assert(App.globalGet(resubmit_key)),
            old_choice_count_key.store(
                SetByte(
                    option_count_keys[0],
                    Int(len(option_count_prefix)),
                    Btoi(sender_box.value()),
                )
            ),
            App.globalPut(
                old_choice_count_key.load(),
                App.globalGet(old_choice_count_key.load()) - Int(1),
            ),
        ),
        App.box_put(Txn.sender(), choice.encode()),
        # ### BOXES END ### #
        App.globalPut(
            new_choice_count_key.load(),
            App.globalGet(new_choice_count_key.load()) + Int(1),
        ),
    )


class PollStatus(abi.NamedTuple):
    can_resubmit: abi.Field[abi.Bool]
    is_open: abi.Field[abi.Bool]
    results: abi.Field[abi.StaticArray[abi.Tuple2[abi.String, abi.Uint64], Literal[3]]]


@router.method
def status(*, output: PollStatus) -> Expr:
    """Get the status of this poll.

    Returns:
        A tuple containing the following information, in order: whether the poll allows
        resubmission, whether the poll is open, and an array of the poll's current results. This
        array contains one entry per option, and each entry is a tuple of that option's value and
        the number of accounts who have voted for it.
    """
    can_resubmit = abi.make(abi.Bool)
    is_open = abi.make(abi.Bool)
    option_name = abi.make(abi.String)
    option_count = abi.make(abi.Uint64)
    partial_results = [
        abi.make(abi.Tuple2[abi.String, abi.Uint64]),
        abi.make(abi.Tuple2[abi.String, abi.Uint64]),
        abi.make(abi.Tuple2[abi.String, abi.Uint64]),
    ]
    results = abi.make(abi.StaticArray[abi.Tuple2[abi.String, abi.Uint64], Literal[3]])
    return Seq(
        can_resubmit.set(App.globalGet(resubmit_key)),
        is_open.set(App.globalGet(open_key)),
        option_name.set(App.globalGet(option_name_keys[0])),
        option_count.set(App.globalGet(option_count_keys[0])),
        partial_results[0].set(option_name, option_count),
        option_name.set(App.globalGet(option_name_keys[1])),
        option_count.set(App.globalGet(option_count_keys[1])),
        partial_results[1].set(option_name, option_count),
        option_name.set(App.globalGet(option_name_keys[2])),
        option_count.set(App.globalGet(option_count_keys[2])),
        partial_results[2].set(option_name, option_count),
        results.set([partial_results[0], partial_results[1], partial_results[2]]),
        output.set(can_resubmit, is_open, results),
    )
In [40]:
approval, clear, json_contract = router.compile_program(version=8, optimize=opts)

JSON Contract Stub - COMPLETE¶

In [41]:
# print(json.dumps(json_contract.dictify(), indent=2))
In [42]:
%%script false --no-raise-error # {
  "name": "OpenPollingApp",
  "methods": [
    {
      "name": "create",
      "args": [
        {
          "type": "string[3]",
          "name": "options",
          "desc": "A list of options for the poll. This list should not contain duplicate entries."
        },
        {
          "type": "bool",
          "name": "can_resubmit",
          "desc": "Whether this poll allows accounts to change their submissions or not."
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Create a new polling application."
    },
    {
      "name": "open",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as open.\nThis will fail if the poll is already open.\nThe poll must be open in order to receive user input."
    },
    {
      "name": "close",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as closed.\nThis will fail if the poll is already closed."
    },
    {
      "name": "submit",
      "args": [
        {
          "type": "uint8",
          "name": "choice",
          "desc": "The choice made by the sender. This must be an index into the options for this poll."
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Submit a response to the poll.\nSubmissions can only be received if the poll is open. If the poll is closed, this will fail.\nIf a submission has already been made by the sender and the poll allows resubmissions, the sender's choice will be updated to the most recent submission. If the poll does not allow resubmissions, this action will fail."
    },
    {
      "name": "status",
      "args": [],
      "returns": {
        "type": "(bool,bool,(string,uint64)[3])",
        "desc": "A tuple containing the following information, in order: whether the poll allows resubmission, whether the poll is open, and an array of the poll's current results. This array contains one entry per option, and each entry is a tuple of that option's value and the number of accounts who have voted for it."
      },
      "desc": "Get the status of this poll."
    }
  ],
  "networks": {},
  "desc": "A polling application with no restrictions on who can participate."
}
In [43]:
show.prepare(json.dumps(json_contract.dictify(), indent=2), (1,65))
In [44]:
show()
{
  "name": "OpenPollingApp",
  "methods": [
    {
      "name": "create",
      "args": [
        {
          "type": "string[3]",
          "name": "options",
          "desc": "A list of options for the poll. This list should not contain duplicate entries."
        },
        {
          "type": "bool",
          "name": "can_resubmit",
          "desc": "Whether this poll allows accounts to change their submissions or not."
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Create a new polling application."
    },
    {
      "name": "open",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as open.\nThis will fail if the poll is already open.\nThe poll must be open in order to receive user input."
    },
    {
      "name": "close",
      "args": [],
      "returns": {
        "type": "void"
      },
      "desc": "Marks this poll as closed.\nThis will fail if the poll is already closed."
    },
    {
      "name": "submit",
      "args": [
        {
          "type": "uint8",
          "name": "choice",
          "desc": "The choice made by the sender. This must be an index into the options for this poll."
        }
      ],
      "returns": {
        "type": "void"
      },
      "desc": "Submit a response to the poll.\nSubmissions can only be received if the poll is open. If the poll is closed, this will fail.\nIf a submission has already been made by the sender and the poll allows resubmissions, the sender's choice will be updated to the most recent submission. If the poll does not allow resubmissions, this action will fail."
    },
    {
      "name": "status",
      "args": [],
      "returns": {
        "type": "(bool,bool,(string,uint64)[3])",
        "desc": "A tuple containing the following information, in order: whether the poll allows resubmission, whether the poll is open, and an array of the poll's current results. This array contains one entry per option, and each entry is a tuple of that option's value and the number of accounts who have voted for it."
      },
      "desc": "Get the status of this poll."
    }
  ],
  "networks": {},
  "desc": "A polling application with no restrictions on who can participate."
}
In [45]:
# print(approval)
In [46]:
%%script false --no-raise-error #pragma version 8
txn NumAppArgs
int 0
==
bnz main_l12
txna ApplicationArgs 0
method "create(string[3],bool)void"
==
bnz main_l11
txna ApplicationArgs 0
method "open()void"
==
bnz main_l10
txna ApplicationArgs 0
method "close()void"
==
bnz main_l9
txna ApplicationArgs 0
method "submit(uint8)void"
==
bnz main_l8
txna ApplicationArgs 0
method "status()(bool,bool,(string,uint64)[3])"
==
bnz main_l7
err
main_l7:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub status_4
store 2
byte 0x151f7c75
load 2
concat
log
int 1
return
main_l8:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
txna ApplicationArgs 1
int 0
getbyte
callsub submit_3
int 1
return
main_l9:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub close_2
int 1
return
main_l10:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub open_1
int 1
return
main_l11:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
==
&&
assert
txna ApplicationArgs 1
store 0
txna ApplicationArgs 2
int 0
int 8
*
getbit
store 1
load 0
load 1
callsub create_0
int 1
return
main_l12:
txn OnCompletion
int DeleteApplication
==
bnz main_l14
err
main_l14:
txn ApplicationID
int 0
!=
assert
txn Sender
global CreatorAddress
==
assert
int 1
return

// create
create_0:
store 20
store 19
byte 0x6f70656e
int 0
app_global_put
byte 0x72657375626d6974
load 20
app_global_put
load 19
load 19
int 2
int 0
*
extract_uint16
int 0
int 1
+
int 3
==
bnz create_0_l8
load 19
int 2
int 0
*
int 2
+
extract_uint16
create_0_l2:
substring3
store 21
byte 0x6f7074696f6e5f6e616d655f00
load 21
extract 2 0
app_global_put
byte 0x6f7074696f6e5f636f756e745f00
int 0
app_global_put
load 19
load 19
int 2
int 1
*
extract_uint16
int 1
int 1
+
int 3
==
bnz create_0_l7
load 19
int 2
int 1
*
int 2
+
extract_uint16
create_0_l4:
substring3
store 21
byte 0x6f7074696f6e5f6e616d655f01
load 21
extract 2 0
app_global_put
byte 0x6f7074696f6e5f636f756e745f01
int 0
app_global_put
load 19
load 19
int 2
int 2
*
extract_uint16
int 2
int 1
+
int 3
==
bnz create_0_l6
load 19
int 2
int 2
*
int 2
+
extract_uint16
b create_0_l9
create_0_l6:
load 19
len
b create_0_l9
create_0_l7:
load 19
len
b create_0_l4
create_0_l8:
load 19
len
b create_0_l2
create_0_l9:
substring3
store 21
byte 0x6f7074696f6e5f6e616d655f02
load 21
extract 2 0
app_global_put
byte 0x6f7074696f6e5f636f756e745f02
int 0
app_global_put
retsub

// open
open_1:
byte 0x6f70656e
app_global_get
!
assert
byte 0x6f70656e
int 1
app_global_put
retsub

// close
close_2:
byte 0x6f70656e
app_global_get
assert
byte 0x6f70656e
int 0
app_global_put
retsub

// submit
submit_3:
store 22
load 22
int 3
<
assert
byte 0x6f7074696f6e5f636f756e745f00
int 13
load 22
setbyte
store 23
txn Sender
box_get
store 26
store 25
load 26
bz submit_3_l2
byte 0x72657375626d6974
app_global_get
assert
byte 0x6f7074696f6e5f636f756e745f00
int 13
load 25
btoi
setbyte
store 24
load 24
load 24
app_global_get
int 1
-
app_global_put
submit_3_l2:
txn Sender
byte 0x00
int 0
load 22
setbyte
box_put
load 23
load 23
app_global_get
int 1
+
app_global_put
retsub

// status
status_4:
byte 0x72657375626d6974
app_global_get
!
!
store 3
byte 0x6f70656e
app_global_get
!
!
store 4
byte 0x6f7074696f6e5f6e616d655f00
app_global_get
store 5
load 5
len
itob
extract 6 0
load 5
concat
store 5
byte 0x6f7074696f6e5f636f756e745f00
app_global_get
store 6
load 5
store 11
int 10
itob
extract 6 0
load 6
itob
concat
load 11
concat
store 7
byte 0x6f7074696f6e5f6e616d655f01
app_global_get
store 5
load 5
len
itob
extract 6 0
load 5
concat
store 5
byte 0x6f7074696f6e5f636f756e745f01
app_global_get
store 6
load 5
store 12
int 10
itob
extract 6 0
load 6
itob
concat
load 12
concat
store 8
byte 0x6f7074696f6e5f6e616d655f02
app_global_get
store 5
load 5
len
itob
extract 6 0
load 5
concat
store 5
byte 0x6f7074696f6e5f636f756e745f02
app_global_get
store 6
load 5
store 13
int 10
itob
extract 6 0
load 6
itob
concat
load 13
concat
store 9
load 7
store 17
load 17
store 16
int 6
store 14
load 14
load 17
len
+
store 15
load 15
int 65536
<
assert
load 14
itob
extract 6 0
load 8
store 17
load 16
load 17
concat
store 16
load 15
store 14
load 14
load 17
len
+
store 15
load 15
int 65536
<
assert
load 14
itob
extract 6 0
concat
load 9
store 17
load 16
load 17
concat
store 16
load 15
store 14
load 14
itob
extract 6 0
concat
load 16
concat
store 10
byte 0x00
int 0
load 3
setbit
int 1
load 4
setbit
load 10
store 18
int 3
itob
extract 6 0
concat
load 18
concat
retsub
In [48]:
show()
#pragma version 8
txn NumAppArgs
int 0
==
bnz main_l12
txna ApplicationArgs 0
method "create(string[3],bool)void"
==
bnz main_l11
txna ApplicationArgs 0
method "open()void"
==
bnz main_l10
txna ApplicationArgs 0
method "close()void"
==
bnz main_l9
txna ApplicationArgs 0
method "submit(uint8)void"
==
bnz main_l8
txna ApplicationArgs 0
method "status()(bool,bool,(string,uint64)[3])"
==
bnz main_l7
err
main_l7:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub status_4
store 2
byte 0x151f7c75
load 2
concat
log
int 1
return
main_l8:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
txna ApplicationArgs 1
int 0
getbyte
callsub submit_3
int 1
return
main_l9:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub close_2
int 1
return
main_l10:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
!=
&&
assert
callsub open_1
int 1
return
main_l11:
txn OnCompletion
int NoOp
==
txn ApplicationID
int 0
==
&&
assert
txna ApplicationArgs 1
store 0
txna ApplicationArgs 2
int 0
int 8
*
getbit
store 1
load 0
load 1
callsub create_0
int 1
return
main_l12:
txn OnCompletion
int DeleteApplication
==
bnz main_l14
err
main_l14:
txn ApplicationID
int 0
!=
assert
txn Sender
global CreatorAddress
==
assert
int 1
return

// create
create_0:
store 20
store 19
byte 0x6f70656e
int 0
app_global_put
byte 0x72657375626d6974
load 20
app_global_put
load 19
load 19
int 2
int 0
*
extract_uint16
int 0
int 1
+
int 3
==
bnz create_0_l8
load 19
int 2
int 0
*
int 2
+
extract_uint16
create_0_l2:
substring3
store 21
byte 0x6f7074696f6e5f6e616d655f00
load 21
extract 2 0
app_global_put
byte 0x6f7074696f6e5f636f756e745f00
int 0
app_global_put
load 19
load 19
int 2
int 1
*
extract_uint16
int 1
int 1
+
int 3
==
bnz create_0_l7
load 19
int 2
int 1
*
int 2
+
extract_uint16
create_0_l4:
substring3
store 21
byte 0x6f7074696f6e5f6e616d655f01
load 21
extract 2 0
app_global_put
byte 0x6f7074696f6e5f636f756e745f01
int 0
app_global_put
load 19
load 19
int 2
int 2
*
extract_uint16
int 2
int 1
+
int 3
==
bnz create_0_l6
load 19
int 2
int 2
*
int 2
+
extract_uint16
b create_0_l9
create_0_l6:
load 19
len
b create_0_l9
create_0_l7:
load 19
len
b create_0_l4
create_0_l8:
load 19
len
b create_0_l2
create_0_l9:
substring3
store 21
byte 0x6f7074696f6e5f6e616d655f02
load 21
extract 2 0
app_global_put
byte 0x6f7074696f6e5f636f756e745f02
int 0
app_global_put
retsub

// open
open_1:
byte 0x6f70656e
app_global_get
!
assert
byte 0x6f70656e
int 1
app_global_put
retsub

// close
close_2:
byte 0x6f70656e
app_global_get
assert
byte 0x6f70656e
int 0
app_global_put
retsub

// submit
submit_3:
store 22
load 22
int 3
<
assert
byte 0x6f7074696f6e5f636f756e745f00
int 13
load 22
setbyte
store 23
txn Sender
box_get
store 26
store 25
load 26
bz submit_3_l2
byte 0x72657375626d6974
app_global_get
assert
byte 0x6f7074696f6e5f636f756e745f00
int 13
load 25
btoi
setbyte
store 24
load 24
load 24
app_global_get
int 1
-
app_global_put
submit_3_l2:
txn Sender
byte 0x00
int 0
load 22
setbyte
box_put
load 23
load 23
app_global_get
int 1
+
app_global_put
retsub

// status
status_4:
byte 0x72657375626d6974
app_global_get
!
!
store 3
byte 0x6f70656e
app_global_get
!
!
store 4
byte 0x6f7074696f6e5f6e616d655f00
app_global_get
store 5
load 5
len
itob
extract 6 0
load 5
concat
store 5
byte 0x6f7074696f6e5f636f756e745f00
app_global_get
store 6
load 5
store 11
int 10
itob
extract 6 0
load 6
itob
concat
load 11
concat
store 7
byte 0x6f7074696f6e5f6e616d655f01
app_global_get
store 5
load 5
len
itob
extract 6 0
load 5
concat
store 5
byte 0x6f7074696f6e5f636f756e745f01
app_global_get
store 6
load 5
store 12
int 10
itob
extract 6 0
load 6
itob
concat
load 12
concat
store 8
byte 0x6f7074696f6e5f6e616d655f02
app_global_get
store 5
load 5
len
itob
extract 6 0
load 5
concat
store 5
byte 0x6f7074696f6e5f636f756e745f02
app_global_get
store 6
load 5
store 13
int 10
itob
extract 6 0
load 6
itob
concat
load 13
concat
store 9
load 7
store 17
load 17
store 16
int 6
store 14
load 14
load 17
len
+
store 15
load 15
int 65536
<
assert
load 14
itob
extract 6 0
load 8
store 17
load 16
load 17
concat
store 16
load 15
store 14
load 14
load 17
len
+
store 15
load 15
int 65536
<
assert
load 14
itob
extract 6 0
concat
load 9
store 17
load 16
load 17
concat
store 16
load 15
store 14
load 14
itob
extract 6 0
concat
load 16
concat
store 10
byte 0x00
int 0
load 3
setbit
int 1
load 4
setbit
load 10
store 18
int 3
itob
extract 6 0
concat
load 18
concat
retsub